﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Diagnostics;

namespace SharedGenomics.TextMining
{
    public class ScoredTermCollection : ICollection<ScoredTerm>
    {
        #region Private Members

        private SortedDictionary<double, ICollection<string>> _terms;
        private Dictionary<char, IList<ScoredTerm>> _startingLetterTerms;

        #endregion

        #region Constructors

        public ScoredTermCollection()
        {
            this._terms = new SortedDictionary<double, ICollection<string>>();
            this._startingLetterTerms = new Dictionary<char, IList<ScoredTerm>>();
        }

        #endregion

        #region Public Methods

        public void Add(string term, double cValue)
        {
            this.Add(new ScoredTerm(term, cValue));
        }

        public ICollection<ScoredTerm> GetTermsStartingWith(char letter)
        {
            char letterToLower = Char.ToLower(letter);
            IList<ScoredTerm> termList = (  this._startingLetterTerms.ContainsKey(letterToLower)) ?
                                            this._startingLetterTerms[letterToLower] :
                                            new List<ScoredTerm>();

            return new ReadOnlyCollection<ScoredTerm>(termList);
        }

        public int GetTermQuartile(ScoredTerm term)
        {
            if(term == null)
                throw new ArgumentNullException("term");

            double cValue = term.CValue;
            double[] quartileBounds = this.GetQuartileBounds();

            Debug.Assert(quartileBounds != null);
            Debug.Assert(quartileBounds.Length == 3);

            for (int i = 0; i < quartileBounds.Length; i++)
            {
                if (cValue < quartileBounds[i])
                    return i + 1;
            }

            //if we get this far the cValue is greater than the minimum for the 4th quartile
            return 4;
        }

        #endregion

        #region Private Methods

        private void AddWordTerm(ScoredTerm term)
        {
            Debug.Assert(term != null);

            if (term.Words.Count > 0 && term.Words[0].Length > 0)
            {
                //get first word and add term to it's collection of associated terms (i.e. terms that start with that word)
                //convert the string to lower case to allow case insensitive matching - when words are checked for terms in the GetTermsStartingWith method 
                //the input word is also converted to lower case for the comparison
                char firstLetter = Char.ToLower(term.Words[0][0]);

                if (!this._startingLetterTerms.ContainsKey(firstLetter))
                {
                    this._startingLetterTerms[firstLetter] = new List<ScoredTerm>();
                }

                this._startingLetterTerms[firstLetter].Add(term);
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns>A double array containing the minimum values for the 2nd, 3rd and 4th quartiles
        /// </returns>
        private double[] GetQuartileBounds()
        {
            double[] quartileBounds = new double[3];

            //the approximate number of elements in each quartile. NOTE: since items with the same cValue must be in the same quartile, there are no guarantees 
            //about how many elements will be in any quartile (some may even be empty) depending on the distribution of the data
            double quartileCount = this.Count / 4.0;

            //the number of elements assigned to a quartile so far
            int assignedCount = 0;
            double cumQuartileLimit = quartileCount;
            int quartileBoundIndex = 0;

            foreach (var kvp in this._terms)
            {
                if (assignedCount >= cumQuartileLimit)
                {
                    //find the next quartile limit greater than the current assignment count
                    while (assignedCount >= cumQuartileLimit)
                    {
                        quartileBounds[quartileBoundIndex] = kvp.Key;
                        quartileBoundIndex++;

                        cumQuartileLimit += quartileCount;
                    }

                    //finished if all the quartile limits have been assigned
                    if (quartileBoundIndex > 2)
                    {
                        break;
                    }
                }

                assignedCount += kvp.Value.Count;
            }

            return quartileBounds;
        }

        #endregion

        #region ICollection<ScoredTerm> Members

        public void Add(ScoredTerm item)
        {
            if (item == null)
                throw new ArgumentNullException("item");

            if (String.IsNullOrEmpty(item.Term))
                throw new ArgumentException("Term cannot be null or empty");

            if (! this.Contains(item))
            {
                double cValue = item.CValue;

                if (!this._terms.ContainsKey(cValue))
                {
                    this._terms[cValue] = new HashSet<string>();
                }

                this._terms[cValue].Add(item.Term);

                //add this term for the first contained word
                this.AddWordTerm(item);
            }
        }

        public void Clear()
        {
            this._terms.Clear();
            this._startingLetterTerms.Clear();
        }

        public bool Contains(ScoredTerm item)
        {
            return (item != null &&
                    this._terms.ContainsKey(item.CValue) &&
                    this._terms[item.CValue].Contains(item.Term));
        }

        public void CopyTo(ScoredTerm[] array, int arrayIndex)
        {
            if (array == null)
                throw new ArgumentNullException("array");

            if (arrayIndex < 0)
                throw new ArgumentOutOfRangeException("arrayIndex");

            if (array.Rank > 1 ||
                arrayIndex >= array.Length ||
                this.Count > arrayIndex + array.Length)
            {
                throw new ArgumentException();
            }

            foreach (ScoredTerm t in this)
                array[arrayIndex++] = new ScoredTerm(t.Term, t.CValue);
        }

        public int Count
        {
            get { return this._terms.Sum(kvp => kvp.Value.Count); }
        }

        public bool IsReadOnly
        {
            get { return false; }
        }

        public bool Remove(ScoredTerm item)
        {
            if (this.Contains(item))
            {
                Debug.Assert(item != null);

                return this._terms[item.CValue].Remove(item.Term);
            }
            else
            {
                return false;
            }
        }

        #endregion

        #region IEnumerable<ScoredTerm> Members

        public IEnumerator<ScoredTerm> GetEnumerator()
        {
            foreach (KeyValuePair<double, ICollection<string>> kvp in this._terms)
            {
                foreach (string term in kvp.Value)
                {
                    yield return new ScoredTerm(term, kvp.Key);
                }
            }
        }

        #endregion

        #region IEnumerable Members

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }

        #endregion
    }
}
